package com.ease.framework {

import com.ease.util.Diagnostics;
import com.ease.util.ReflectionUtil;
import com.ease.util.Set;
import com.ease.util.events.MethodReturnEvent;
import com.ease.util.gc.SharedReference;

import flash.events.Event;
import flash.utils.Dictionary;

import mx.utils.StringUtil;

/**
 * This class is responsible for creation and/or invocation of commnand objects or
 * methods. It maintains command maps, which are maps from request event ids to command
 * class type and creates instances of these classes (or just invoke static methods) as
 * request events appear on the associated event bus.
 *
 * <p>The simplest way to write commands is in the form of a class with one or more
 * static methods, each of which targets a specific request type. These methods are
 * marked with the [RequestHandler] tag and being static, they take their entire context
 * as parameters. The request event is always the first parameter and the last parameter
 * can optionally be a callback (which takes MethodReturnEvent as its arg). There can be
 * any number of parameters between the first arg and the optional last arg, values for
 * which are looked up or created as necessary by Frinje. These other parameters are
 * usually the targets of the command task, e.g. proxy objects.
 *
 * <listing version="3.0">
 * public class StaticCommands
 * {
 *    [RequestHandler("someRequest")]
 *    public static function onSomeRequest( request:SomeRequestEvent,
 *                                          target:ISomeProxy,
 *                                          callback:Function ):void {
 *       target.performSomeTask( request.someProp, callback );
 *    }
 *    // ...
 * }
 * </listing>
 *
 * <p> This class could be registered using CommandRegistry's addCommand method at app or
 * module initialization, or could be registered through a CommandSet just like any other
 * command class.
 *
 * <p>A relatively more complex command can be implemented as a class whose instances are
 * created specially to service individual request events. Such a command class may look
 * like the following:
 *
 * <listing version="3.0">
 * public class SomeCommand
 * {
 *    [InjectableProxy]
 *    public var proxy:ISomeProxy;
 *    // ...
 *    [InjectableRequest]
 *    public var request:SomeRequestEvent;
 *    // ...
 *    [RequestHandler("someRequest")]
 *    public function execute():void {
 *       proxy.performSomeTask( request.whatever,
 *                              CallbackUtil.dispatchResponseUsingRequestContext,
 *                              request );
 *    }
 * }
 * </listing>
 *
 * The command class above could also implement ICommand explicitly. Or, it could instead
 * use one or more request handlers to handle the request events, as illustrated in the
 * following example:
 *
 * <listing version="3.0">
 * public class SomeCommand
 * {
 *    [InjectableProxy]
 *    public var proxy:ISomeProxy;
 *    // ...
 *    [RequestHandler("someRequest")]
 *    public function onSomeRequest( request:SomeRequestEvent ):void {
 *       // ...
 *    }
 *    // ...
 *    [RequestHandler("someOtherRequest")]
 *    public function onSomeOtherRequest( request:SomeOtherRequestEvent ):void {
 *       // ...
 *    }
 * }
 * </listing>
 *
 * <p>The command class could also provide its own callback, as shown below, as opposed
 * to using the one from CallbackUtil:
 *
 * <listing version="3.0">
 * public function execute():void {
 *    proxy.performSomeTask( request.whatever, onProxyReturn );
 * }
 * // ...
 * private function onProxyReturn( ev:MethodReturnEvent ):void {
 *    // ...
 * }
 * </listing>
 *
 * <p>If the command class is an <code>IEventDispatcher</code> (e.g., if it extends
 * <code>EventDispatcher</code>), it could also raise the response event (rather than
 * dispatching it through <code>CallbackUtil</code>).
 *
 * <listing version="3.0">
 * [Event(name="someReturn", type="com.some.ns.events.SomeResponseEvent")]
 * public class SomeCommand extends EventDispatcher
 * {
 *    // ...
 *    public function execute():void {
 *       proxy.performSomeTask( request.whatever, onProxyReturn );
 *    }
 *    // ...
 *    private function onProxyReturn( ev:MethodReturnEvent ):void {
 *       var response:SomeResponseEvent = ...;
 *       // ...
 *       response.request = request;
 *       dispatchEvent( response );
 *    }
 * }
 * </listing>
 *
 * <p>The above example is an execute style command, but it could just as well be a
 * request-handler style command and the EventDispatcher part would still apply.
 *
 * <p>The proxy interface referenced in the above examples (i.e., ISomeProxy) may have
 * multiple implementations. The only requirement for injectability purposes is that the
 * interface should declare a [DefaultImplementation].
 *
 * <p>If a command object invokes other commands (through request events, as opposed to
 * directly invoking proxy or any other methods with callbacks, for example), and the
 * request event definition doesn't have the form (or metadata annotations) that EventBus
 * prescribes for the request-response pattern, then the dispatching command needs to take
 * responsibility for its own lifetime so that it would stick around while those other
 * commands are in progress. Here's one way of accomplishing this:
 * <ol>
 *   <li>In the command constructor, let the frameworks (i.e., frinje, etc) know that this
 *       object exists by raising an event through ObjectEvent.newAddition. This event can
 *       be raised either on the command itself (if it's an EventDispatcher, which it
 *       would be if it's to receive responses from other commands), or on the
 *       Application.
 *       This would make sure that the command gets registered on the event bus in a
 *       strong sense. That would also allow the use of ResponseHandler tag in this
 *       command class, as well as alleviate the weak-reference/lifetime issues.
 *   <li>When the whole command execution is done, raise the object removal event using
 *       ObjectEvent.newRemoval, so that the object gets unregistered from the framework
 *       services.
 * </ol>
 *
 * <p>The request-response pattern is usually used with the (short-lived) command objects,
 * but it can also be used by objects of indefinite lifetime that already exist at the
 * time of request event dispatch. No commands need to be registered in that case, and the
 * regular [EventHandler] tag is used (rather than the [RequestHandler] tag) to mark the
 * request handler method.
 *
 * @see EventBus
 * @see ProxyRegistry
 * @see CallbackUtil
 * @see ICommand
 * @see MethodReturnEvent
 */
// XXX (nice to have): Introduce a ICommandFactory interface that this class impl's
public class CommandRegistry
{
   private static const CONVENTIONAL_CMD_METHOD_NAME:String = "execute";
   private static var _instance:CommandRegistry;
   private var _mappings:Dictionary = new Dictionary();
   private var _injectionMgrRef:SharedReference = SharedReference.newReference(InjectionManager);
   private var _eventBusRef:SharedReference = SharedReference.newReference(EventBus);
   private var _proxyRegistryRef:SharedReference = SharedReference.newReference(ProxyRegistry);

   /**
    * A global instance of this class. This is the instance that would typically be used
    * by an application.
    *
    * @return
    *    An instance of CommandRegistry for global use.
    */
   public static function get instance():CommandRegistry {
      if (_instance == null) {
         _instance = new CommandRegistry( EventBus.instance, ProxyRegistry.instance );
      }
      return _instance;
   }

   /**
    * Initializes a new instance of this class.
    *
    * @param eventBus
    *    The event bus to associate with the new command registry.
    *
    * @param proxyRegistry
    *    The proxy registry to associate with the new command registry.
    */
   public function CommandRegistry( eventBus:EventBus = null,
                                    proxyRegistry:ProxyRegistry = null ) {
      if (eventBus == null) {
         eventBus = new EventBus();
      }
      setEventBus(eventBus);
      if (proxyRegistry == null) {
         proxyRegistry = new ProxyRegistry( this.eventBus );
      }
      setProxyRegistry(proxyRegistry);
      this.injectionMgr = this.proxyRegistry.injectionMgr;
   }

   //------------------------------------------------------------------------------------
   // Instance properties
   // The property provider used avoids directly referencing the global instance.
   // This is done to simplify the object graph for Memory Profiler.

   //-------------------------------------------------------
   // Property: eventBus

   /**
    * The EventBus associated with this registry. This registry would respond to the
    * request events on this bus by creating commands and posting back their responses.
    *
    * @return
    *    The associated event bus.
    */
   public function get eventBus():EventBus {
      return _eventBusRef.object;
   }

   /**
    * @private
    */
   private function setEventBus(value:EventBus):void {
      _eventBusRef.object = value;
   }


   //-------------------------------------------------------
   // Property: proxyRegistry

   /**
    * The proxy registry associated with this command registry. This proxy registry would
    * be used to inject proxies into the commands created by this command registry.
    *
    * @return
    *    The associated proxy registry.
    */
   public function get proxyRegistry():ProxyRegistry {
      return _proxyRegistryRef.object;
   }


   /**
    * @private
    */
   private function setProxyRegistry(value:ProxyRegistry):void {
      _proxyRegistryRef.object = value;
   }

   //-------------------------------------------------------
   // Property: injectionManager

   private function get injectionMgr():InjectionManager {
      return _injectionMgrRef.object;
   }

   private function set injectionMgr(value:InjectionManager):void {
      _injectionMgrRef.object = value;
   }


   /**
    * Adds a CommandSet instance to this registry. A CommandSet contains references to a
    * bunch of command classes in the app.
    *
    * @param cmdSet
    *    The command set to be added.
    */
   public final function addCommandSet( cmdSet:CommandSet ):void {
      if (cmdSet == null) {
         return;
      }
      // Avoiding reflection (describeType) on the command class for performance
      // concerns.
      for each (var mapping:CommandMapping in cmdSet.mappings) {
         addCommand( mapping.command, mapping.request );
      }
      // We don't know the request IDs for a straight list of Command classes, so need
      // reflection here.
      for each (var cmdClass:Class in cmdSet.commands) {
         var requestHandlerCount:uint = registerAllCmdsProvidedBy( cmdClass );
         if (requestHandlerCount == 0) {
            Diagnostics.assert( false, "No [RequestHandler]s found in: " + cmdClass );
         }
      }
      if (cmdSet.registry == null) {
         cmdSet.registry = this;
      }
   }

   /**
    * Adds command mappings for the given command class. If a request id is provided, the
    * command is assumed to handle a single request type. Otherwise, the [RequestHandler]
    * methods in the class are looked up for the applicable request IDs.
    *
    * @param commandClass
    *    A command class that handles one or more request types.
    *
    * @param requestEventId
    *    The request event id to add the command for, so that an event of this type on
    *    the event bus would translate to the given command.
    */
   public final function addCommand( commandClass:Class,
                                     requestEventId:String = null ):void {
      if (requestEventId != null) {
         registerCmdForRequest( commandClass, requestEventId );
      } else {
         registerAllCmdsProvidedBy( commandClass );
      }
   }

   /**
    * Removes the association of the given request event id with the given command class.
    * If no command class is specified, all commands registered for the given event id
    * are removed.
    *
    * @param requestEventId
    *    The request event id whose commands are to be removed.
    *
    * @param commandClass
    *    The command that should be removed for the given request id.
    */
   public final function removeCommand( requestEventId:String,
                                        commandClass:Class = null ):void {
      var registeredCmd:Object = _mappings[requestEventId];
      if (registeredCmd == null) {
         return;
      }
      if (commandClass == null || registeredCmd == commandClass) {
         // unregister from the bus for the given event
         this.eventBus.removeEventListener( requestEventId, onCommandRequest );
         delete _mappings[requestEventId];
         return;
      }
      if (registeredCmd is Set) {
         var cmdSet:Set = Set( registeredCmd );
         cmdSet.remove( commandClass );
         if (cmdSet.length == 0) {
            delete _mappings[requestEventId];
         }
         return;
      }
   }

   /**
    * Creates a new instance of the given class and initializes it by setting certain
    * properties, such as those tagged as [InjectableProxy].
    *
    * @param commandClass
    *    The type whose instance is desired.
    *
    * @return
    *    A new command instance of the given type.
    */
   public final function newCommand( commandClass:Class ):Object {
      return createAndSetupCommand( commandClass );
   }

   private function registerCmdForRequest( commandClass:Class,
                                           requestEventId:String ):void {
      var currentlyRegisteredCmd:Object = _mappings[requestEventId];
      if (commandClass == currentlyRegisteredCmd) {
         // duplicate registrations of the same command are ignored
         return;
      }
      if (currentlyRegisteredCmd is Set) {
         Set( currentlyRegisteredCmd ).add( commandClass );
         return;
      }
      if (currentlyRegisteredCmd) {
         var newCmdSet:Set = new Set();
         newCmdSet.add( currentlyRegisteredCmd );
         newCmdSet.add( commandClass );
         _mappings[requestEventId] = newCmdSet;
         return;
      }
      // first time registration for this command request id
      _mappings[requestEventId] = commandClass;
      // register with the bus for the given event
      this.eventBus.addEventListener( requestEventId, onCommandRequest );
   }

   /**
    * @return
    *    The number of request IDs that the command is registered for.
    */
   private function registerAllCmdsProvidedBy( commandClass:Class ):uint {
      var requestIDs:Array =
            ReflectionUtil.getMethodTagValues( commandClass, "RequestHandler" );
      for each (var requestId:String in requestIDs) {
         registerCmdForRequest( commandClass, requestId );
      }
      return requestIDs.length;
   }

   private function onCommandRequest( request:Event ):void {
      var command:Object = _mappings[request.type];
      if (command is Set) {
         Set( command ).forEach( execute, request );
      } else if (command is Class) {
         execute( Class( command ), request );
      }
   }

   /**
    * All command executions (regardless of whether the command is an object or a static
    * method) must go through this helper method.
    */
   private function execute( commandClass:Class, request:Event ):void {
      if (ReflectionUtil.hasMethod( commandClass, CONVENTIONAL_CMD_METHOD_NAME )) {
         if (commandClass.hasOwnProperty( CONVENTIONAL_CMD_METHOD_NAME )) {
            // class or static method
            commandClass[CONVENTIONAL_CMD_METHOD_NAME]();
         } else {
            // instance method
            var command:Object = createAndSetupCommand( commandClass, request );
            command[CONVENTIONAL_CMD_METHOD_NAME]();
         }
         return;
      }
      // There's no "execute" method in this command; invoke the explicitly tagged
      // request/event handler, if any.
      var evHandlerName:String = ReflectionUtil.getSingleTaggedMethod(
            commandClass, "RequestHandler", request.type );
      invokeRequestHandler( commandClass, evHandlerName, request );
   }

   private function invokeRequestHandler( cmdClass:Class, methodName:String,
                                          request:Event ):void {
      var target:Object;
      if (cmdClass.hasOwnProperty( methodName )) {
         target = cmdClass;
      } else {
         target = createAndSetupCommand( cmdClass, request );
         if (! target.hasOwnProperty( methodName )) {
            return;
         }
      }
      var argTypes:Array/*Class*/ =
            ReflectionUtil.getArgTypes( cmdClass, target, methodName );
      // The 1st arg, if any, is always supposed to be the original request event; so we
      // validate that first.
      if (argTypes.length > 0 && !(request is argTypes[0])) {
         // Note that this is not supposed to be an assertion or alert. Two different
         // modules could be using the same event id but different event classes. This
         // is a legit case, and should not cause an error. But an informational message
         // could be helpful.
         var msg:String = StringUtil.substitute(
               "CmdRegistry: skipped invocation of {0}.{1} due to type incompatibility",
               cmdClass, methodName );
         trace( msg );
         return;
      }
      // Perform invocation based on the number of arguments.

      if (argTypes.length == 0) {
         // a no-arg method
         target[methodName]();
         return;
      }
      if (argTypes.length == 1) {
         // 1-arg request handlers are supposed to take the request event as their only
         // arg.
         target[methodName]( request );
         return;
      }
      invokeMultiArgRequestHandler( target, methodName, argTypes, request );
   }

   /**
    * Handles multi-arg command methods. 1st arg is always the request event; last arg
    * can optionally be a callback; and there can be any number of args in b/w, which are
    * looked up by their types from the ProxyRegistry.
    */
   private function
   invokeMultiArgRequestHandler( target:Object, methodName:String,
                                 argTypes:Array/*Class*/, request:Event ):void {
      var args:Array/*Object*/ = new Array( argTypes.length );
      args[0] = request;
      var proxyArgStartIndex:int = 1;
      var proxyArgEndIndex:int = argTypes.length - 1;
      if (argTypes[proxyArgEndIndex] == Function) {
         // the last argument to this request handler is a callback
         args[proxyArgEndIndex] = function( returnEv:MethodReturnEvent ):void {
            CallbackUtil.dispatchResponse( request, returnEv );
         }
         proxyArgEndIndex -= 1;
      }
      for (var i:int = proxyArgStartIndex; i <= proxyArgEndIndex; i++) {
         args[i] = this.proxyRegistry.getProxyByType( argTypes[i] );
      }
      var cmdMethod:Function = target[methodName];
      // invoke the request handler
      cmdMethod.apply( target, args );
   }

   private function createAndSetupCommand( commandClass:Class,
                                           request:Event = null ):Object {
      var command:Object = new commandClass;
      // set the proxies on the command
      this.proxyRegistry.setInjectableProxiesOn( command );
      // get the [Injectable] properties processed
      this.injectionMgr.injectTargetsUsingProviders( [command], undefined );

      if (request != null) {
         // Set the [InjectableRequest] (or equivalently, the [Request]) property.
         ReflectionUtil.setTaggedPropertyValue( command, "InjectableRequest|Request",
                                                request );
      }
      // weak-register with the event bus
      this.eventBus.addParticipant( command, true );
      return command;
   }
} // class

}